package ga;

import ga.cross.Anular;
import ga.cross.ClassicCross;
import ga.cross.CrossMethod;
import ga.cross.Multiple;
import ga.cross.Segment;
import ga.individuals.Individual;
import ga.mutation.ClassicMutation;
import ga.mutation.MutationMethod;
import ga.mutation.NotUniform;
import ga.selectionMethods.Boltzman;
import ga.selectionMethods.Elite;
import ga.selectionMethods.EliteBoltzman;
import ga.selectionMethods.EliteRoulette;
import ga.selectionMethods.Roulette;
import ga.selectionMethods.SelectionReplacementMethod;
import ga.selectionMethods.Universal;
import ga.stopcriterias.ChooseStopCriteria;
import ga.stopcriterias.ContentStrategy;
import ga.stopcriterias.GenerationStrategy;
import ga.stopcriterias.OptimalStrategy;
import ga.stopcriterias.StopStrategy;
import ga.stopcriterias.StructureStrategy;

import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Random;

import org.apache.commons.math.stat.descriptive.moment.StandardDeviation;
import org.apache.log4j.Logger;

import com.mathworks.toolbox.javabuilder.MWClassID;
import com.mathworks.toolbox.javabuilder.MWException;
import com.mathworks.toolbox.javabuilder.MWNumericArray;

public class GARun {
	/* En JAVA un double cuenta con 64 bits (8 bytes)*/
	public static final Integer networkWeightSize = 501;
	//public static final Integer bitsInDouble = 64;
	public static final Integer maxIndividualValue = 1;
	public static final Integer minIndividualValue = -1;
	public static Integer popSize, nOfParents, generationsMax, nForElite;
	public static Double mutationProb, crossProb;
	public static String selectionMethod, replacementMethod, mutationMethod, crossMethod, stopCriteria, fitnessMethod;
	public static StandardDeviation deviation = new StandardDeviation();
	private static final Logger LOGGER = Logger.getLogger(GARun.class);
	
//	pop = PopulationInit();
//	currGeneration = 0;
//	while(currGeneration < MAX_GENERATIONS){
//		parents = Selecction(pop);
//		offsprings = Reproducction(parents);
//		pop = Replacement(pop, offsprings);
//	}
	
	public static void main(String[] args) {
		Random r = new Random(System.currentTimeMillis());
		if (args.length == 0) {
			LOGGER.error("No configuration file!");
			return;
		}
		Properties prop = new Properties();
	    String fileName = args[0];
	    InputStream is = null;
		try {
			is = new FileInputStream(fileName);
		} catch (FileNotFoundException e) {
			LOGGER.error("File: " + fileName + " doesn't exist!");
			System.exit(1);
		}

	    try {
			prop.load(is);
		} catch (IOException e) {
			LOGGER.error("Cannot load configuration's file properties!");
			System.exit(1);
		}
		
		if (!getConfigFileParameters(prop)) {
			System.exit(1);
		}
		
		Collection<Individual> population;
		try {
			population = generatePopulation();
		
			List<Individual> offsprings = new ArrayList<Individual> ();
			List<Individual> parents = new ArrayList<Individual> ();
			int currentGeneration = 1;
			
			StopStrategy stopStrategy = getStopStrategy();
			while (!stopStrategy.execute(population)) {
				LOGGER.info("Best: " + Collections.max(population));
				LOGGER.info("Diversity: " + Math.ceil(getDiversity(population)));
				/**
				 * SELECCION
				 */
				LOGGER.info("Generation: " + currentGeneration);
				SelectionReplacementMethod selection = getSelectionReplacementMethod(GARun.selectionMethod, GARun.nOfParents);
				parents.addAll(selection.method(population));
				
				/**
				 * REPRODUCCION
				 */
				
				CrossMethod cross = getCrossMethod();
				while (offsprings.size()<parents.size()){
					Collections.shuffle(parents); // desorganiza la lista para elegir pares de padres al azar para la cruza
					List<Individual> pairParents = new LinkedList<Individual> ();
					pairParents.add(parents.get(1));
					pairParents.add(parents.get(2));

					if (r.nextDouble() < GARun.crossProb){
						offsprings.addAll(cross.method(pairParents));	
					}					
				}
				/*
				Collections.shuffle(parents); // desorganiza la lista para elegir pares de padres al azar para la cruza
                CrossMethod cross = getCrossMethod();
                for (int i = 0 ; i < parents.size() - 2 ; i+=2) {
                        List<Individual> pairParents = new LinkedList<Individual> ();
                        pairParents.add(parents.get(i));
                        pairParents.add(parents.get(i+1));
                        
                        offsprings.addAll(cross.method(pairParents));
                }
				*/
				
				MutationMethod mutation = getMutationMethod();
				Collection<Individual> mutatedOffsprings = mutation.method(offsprings);
				offsprings.clear();
				
				/**
				 * REEMPLAZO
				 */
				SelectionReplacementMethod replacement = getSelectionReplacementMethod(GARun.replacementMethod, GARun.popSize);
				Collection<Individual> populationForReplacement = new LinkedList<Individual>(population);
				populationForReplacement.addAll(mutatedOffsprings);
				population = replacement.method(populationForReplacement);
				
				currentGeneration++;
				parents.clear();
			}
		
			/**
			 * El siguiente codigo escribe un populationInfo.dat con los datos de la poblacion resultante, en caso de que
			 * se desee utilizar dicho archivo para algun proposito
			 */
			
			try {
				BufferedWriter bw = new BufferedWriter(new FileWriter("populationInfo.dat"));
				for (Individual i: population) {
					System.out.println(i);
					bw.write(i.getIndividualValue() + "\t" + i.getFitnessValue() + "\n");
				}
				bw.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			
			/***************************************************************************************************/
			
			LOGGER.info("Best: " + Collections.max(population));
			LOGGER.info("Number of individuals: " + population.size());
			LOGGER.info("Total Generations: " + (currentGeneration-1));
		} catch (MWException e1) {
			e1.printStackTrace();
		}
	}

	private static StopStrategy getStopStrategy() {
		StopStrategy stopStrategy;
		
		switch(ChooseStopCriteria.valueOf(stopCriteria)) {
			case GENERATION:
				stopStrategy = new GenerationStrategy(GARun.generationsMax);
				break;
			case OPTIMAL:
				stopStrategy = new OptimalStrategy(-0.001);
				break;
			case CONTENT:
				stopStrategy = new ContentStrategy(0.00001);
				break;
			case STRUCTURE:
				stopStrategy = new StructureStrategy(0.12);
				break;
			default:
				stopStrategy = new GenerationStrategy(GARun.generationsMax);
		}
		
		return stopStrategy;
	}

	private static double[] getAllFitness(Collection<Individual> individuals){
		double[] allFitness = new double[GARun.popSize];
		int i=0;
		for(Individual individual : individuals){
			allFitness[i++] = individual.getFitnessValue(); 
		}
		return allFitness;
	}
	
	private static Double getDiversity(Collection<Individual> individuals){
		double[] allFitness = getAllFitness(individuals);
		return GARun.deviation.evaluate(allFitness);
	}
	
	private static MutationMethod getMutationMethod() {
		if (GARun.mutationMethod.toUpperCase().equals("CLASSIC")) {
			return new ClassicMutation();
		} else if (GARun.mutationMethod.toUpperCase().equals("NOT_UNIFORM")) {
			return new NotUniform();
		}
		return null;
	}

	private static CrossMethod getCrossMethod() {
		if (GARun.crossMethod.toUpperCase().equals("CLASSIC")) {
			return new ClassicCross();
		} else if (GARun.crossMethod.toUpperCase().equals("ANULAR")) {
			return new Anular();
		} else if (GARun.crossMethod.toUpperCase().equals("MULTIPLE")) {
			return new Multiple();
		} else if (GARun.crossMethod.toUpperCase().equals("SEGMENT")) {
			return new Segment();
		} else {
			return null;
		}
	}

	private static SelectionReplacementMethod getSelectionReplacementMethod(String selectionReplacement, int numberOfElements) {
		if (selectionReplacement.toUpperCase().equals("ELITE")) {
			return new Elite(numberOfElements);
		} else if (selectionReplacement.toUpperCase().equals("ROULETTE")) {
			return new Roulette(numberOfElements);
		} else if (selectionReplacement.toUpperCase().equals("UNIVERSAL")) {
			return new Universal(numberOfElements);
		} else if (selectionReplacement.toUpperCase().equals("BOLTZMAN")) {
			return new Boltzman(numberOfElements);
		} else if (selectionReplacement.toUpperCase().equals("ELITE_BOLTZMAN")) {
			return new EliteBoltzman(GARun.nForElite, numberOfElements);
		} else if (selectionReplacement.toUpperCase().equals("ELITE_ROULETTE")) {
			return new EliteRoulette(GARun.nForElite, numberOfElements);
		} else {
			return null;
		}
	}

	private static Collection<Individual> generatePopulation() throws MWException {
		Collection<Individual> individuals = new LinkedList<Individual>();
		Random r = new Random(System.currentTimeMillis());
		r.setSeed(System.currentTimeMillis());
		
		double[] data = new double[501];
		
		for (int i = 0; i < GARun.popSize; i++) {
			// Los pesos de la red son 501 en total (arquitectura 20 20 1)
			for (int j = 0 ; j < 501 ; j++) {
				data[j] = r.nextDouble() * (GARun.maxIndividualValue - GARun.minIndividualValue) + GARun.minIndividualValue;
			}

			Individual birth = new Individual(new MWNumericArray(data, MWClassID.DOUBLE));
			individuals.add(birth);
		}
		return individuals;
	}

	private static boolean getConfigFileParameters(Properties prop) {
		try {
			GARun.popSize = Integer.valueOf(prop.getProperty("popSize"));
			GARun.nOfParents = Integer.valueOf(prop.getProperty("nOfParents"));
			GARun.generationsMax = Integer.valueOf(prop.getProperty("generationsMax"));
			GARun.mutationProb = Double.valueOf(prop.getProperty("mutationProb"));
			GARun.crossProb = Double.valueOf(prop.getProperty("crossProb"));
			GARun.selectionMethod = prop.getProperty("selectionMethod");
			GARun.replacementMethod = prop.getProperty("replacementMethod");
			GARun.crossMethod = prop.getProperty("crossMethod");
			GARun.mutationMethod = prop.getProperty("mutationMethod");
			GARun.stopCriteria = prop.getProperty("stopCriteria");
			GARun.nForElite = Integer.valueOf(prop.getProperty("nForElite"));
		} catch(Exception e) {
			LOGGER.error("Configuration File error!");
			return false;
		}
		if (GARun.popSize == null || GARun.popSize <= 0 ||
				GARun.nOfParents == null || GARun.nOfParents <= 0 ||
				GARun.generationsMax == null || GARun.generationsMax <= 0 ||
				GARun.mutationProb == null || GARun.mutationProb <= 0.0 || GARun.mutationProb > 1 ||
				GARun.crossProb == null || GARun.crossProb <= 0.0 || GARun.crossProb > 1 ||
				GARun.selectionMethod == null || GARun.selectionMethod.isEmpty() ||
				GARun.crossMethod == null || GARun.crossMethod.isEmpty() || 
				GARun.mutationMethod == null || GARun.mutationMethod.isEmpty() ||
				GARun.replacementMethod == null || GARun.replacementMethod.isEmpty()) {
			LOGGER.error("Configuration File error!");
			return false;
		}
		return true;
	}
}